import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
  DataProviderInterface,
  GetAssetProfileParams,
  GetDividendsParams,
  GetHistoricalParams,
  GetQuotesParams,
  GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
  DEFAULT_CURRENCY,
  REPLACE_NAME_PARTS
} from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import {
  DataProviderHistoricalResponse,
  DataProviderInfo,
  DataProviderResponse,
  LookupItem,
  LookupResponse
} from '@ghostfolio/common/interfaces';
import { MarketState } from '@ghostfolio/common/types';

import { Injectable, Logger } from '@nestjs/common';
import {
  AssetClass,
  AssetSubClass,
  DataSource,
  SymbolProfile
} from '@prisma/client';
import { addDays, format, isSameDay, isToday } from 'date-fns';
import { isNumber } from 'lodash';

@Injectable()
export class EodHistoricalDataService implements DataProviderInterface {
  private apiKey: string;
  private readonly URL = 'https://eodhistoricaldata.com/api';

  public constructor(
    private readonly configurationService: ConfigurationService,
    private readonly symbolProfileService: SymbolProfileService
  ) {
    this.apiKey = this.configurationService.get('API_KEY_EOD_HISTORICAL_DATA');
  }

  public canHandle() {
    return true;
  }

  public async getAssetProfile({
    requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
    symbol
  }: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
    const [searchResult] = await this.getSearchResult({
      requestTimeout,
      query: symbol
    });

    if (!searchResult) {
      return undefined;
    }

    return {
      symbol,
      assetClass: searchResult.assetClass,
      assetSubClass: searchResult.assetSubClass,
      currency: this.convertCurrency(searchResult.currency),
      dataSource: this.getName(),
      isin: searchResult.isin,
      name: searchResult.name
    };
  }

  public getDataProviderInfo(): DataProviderInfo {
    return {
      dataSource: DataSource.EOD_HISTORICAL_DATA,
      isPremium: true,
      name: 'EOD Historical Data',
      url: 'https://eodhd.com'
    };
  }

  public async getDividends({
    from,
    requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
    symbol,
    to
  }: GetDividendsParams): Promise<{
    [date: string]: DataProviderHistoricalResponse;
  }> {
    symbol = this.convertToEodSymbol(symbol);

    if (isSameDay(from, to)) {
      to = addDays(to, 1);
    }

    try {
      const queryParams = new URLSearchParams({
        api_token: this.apiKey,
        fmt: 'json',
        from: format(from, DATE_FORMAT),
        to: format(to, DATE_FORMAT)
      });

      const response: {
        [date: string]: DataProviderHistoricalResponse;
      } = {};

      const historicalResult = await fetch(
        `${this.URL}/div/${symbol}?${queryParams.toString()}`,
        {
          signal: AbortSignal.timeout(requestTimeout)
        }
      ).then((res) => res.json());

      for (const { date, value } of historicalResult) {
        response[date] = {
          marketPrice: value
        };
      }

      return response;
    } catch (error) {
      Logger.error(
        `Could not get dividends for ${symbol} (${this.getName()}) from ${format(
          from,
          DATE_FORMAT
        )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`,
        'EodHistoricalDataService'
      );

      return {};
    }
  }

  public async getHistorical({
    from,
    granularity = 'day',
    requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
    symbol,
    to
  }: GetHistoricalParams): Promise<{
    [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
  }> {
    symbol = this.convertToEodSymbol(symbol);

    try {
      const queryParams = new URLSearchParams({
        api_token: this.apiKey,
        fmt: 'json',
        from: format(from, DATE_FORMAT),
        period: granularity,
        to: format(to, DATE_FORMAT)
      });

      const response = await fetch(
        `${this.URL}/eod/${symbol}?${queryParams.toString()}`,
        {
          signal: AbortSignal.timeout(requestTimeout)
        }
      ).then((res) => res.json());

      return response.reduce(
        (result, { adjusted_close, date }) => {
          if (isNumber(adjusted_close)) {
            result[this.convertFromEodSymbol(symbol)][date] = {
              marketPrice: adjusted_close
            };
          } else {
            Logger.error(
              `Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`,
              'EodHistoricalDataService'
            );
          }

          return result;
        },
        { [this.convertFromEodSymbol(symbol)]: {} }
      );
    } catch (error) {
      throw new Error(
        `Could not get historical market data for ${symbol} (${this.getName()}) from ${format(
          from,
          DATE_FORMAT
        )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
      );
    }
  }

  public getMaxNumberOfSymbolsPerRequest() {
    // It is not recommended using more than 15-20 tickers per request
    // https://eodhistoricaldata.com/financial-apis/live-realtime-stocks-api
    return 20;
  }

  public getName(): DataSource {
    return DataSource.EOD_HISTORICAL_DATA;
  }

  public async getQuotes({
    requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
    symbols
  }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
    const response: { [symbol: string]: DataProviderResponse } = {};

    if (symbols.length <= 0) {
      return response;
    }

    const eodHistoricalDataSymbols = symbols.map((symbol) => {
      return this.convertToEodSymbol(symbol);
    });

    try {
      const queryParams = new URLSearchParams({
        api_token: this.apiKey,
        fmt: 'json',
        s: eodHistoricalDataSymbols.join(',')
      });

      const realTimeResponse = await fetch(
        `${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?${queryParams.toString()}`,
        {
          signal: AbortSignal.timeout(requestTimeout)
        }
      ).then((res) => res.json());

      const quotes: {
        close: number;
        code: string;
        previousClose: number;
        timestamp: number;
      }[] =
        eodHistoricalDataSymbols.length === 1
          ? [realTimeResponse]
          : realTimeResponse;

      const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
        symbols.map((symbol) => {
          return {
            symbol,
            dataSource: this.getName()
          };
        })
      );

      for (const { close, code, previousClose, timestamp } of quotes) {
        let currency: string;

        if (this.isForex(code)) {
          currency = this.convertFromEodSymbol(code)?.replace(
            DEFAULT_CURRENCY,
            ''
          );
        }

        if (!currency) {
          currency = symbolProfiles.find(({ symbol }) => {
            return symbol === code;
          })?.currency;
        }

        if (!currency) {
          const { items } = await this.search({ query: code });

          if (items.length === 1) {
            currency = items[0].currency;
          }
        }

        if (isNumber(close) || isNumber(previousClose)) {
          const marketPrice: number = isNumber(close) ? close : previousClose;
          let marketState: MarketState = 'closed';

          if (this.isForex(code) || isToday(new Date(timestamp * 1000))) {
            marketState = 'open';
          } else if (!isNumber(close)) {
            marketState = 'delayed';
          }

          response[this.convertFromEodSymbol(code)] = {
            currency,
            marketPrice,
            marketState,
            dataSource: this.getName()
          };
        } else {
          Logger.error(
            `Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`,
            'EodHistoricalDataService'
          );
        }
      }

      return response;
    } catch (error) {
      let message = error;

      if (['AbortError', 'TimeoutError'].includes(error?.name)) {
        message = `RequestError: The operation to get the quotes for ${symbols.join(
          ', '
        )} was aborted because the request to the data provider took more than ${(
          this.configurationService.get('REQUEST_TIMEOUT') / 1000
        ).toFixed(3)} seconds`;
      }

      Logger.error(message, 'EodHistoricalDataService');
    }

    return {};
  }

  public getTestSymbol() {
    return 'AAPL.US';
  }

  public async search({
    query,
    requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
  }: GetSearchParams): Promise<LookupResponse> {
    const searchResult = await this.getSearchResult({ query, requestTimeout });

    return {
      items: searchResult
        .filter(({ currency, symbol }) => {
          // Remove 'NA' currency and exchange rates
          return currency?.length === 3 && !this.isForex(symbol);
        })
        .map(
          ({
            assetClass,
            assetSubClass,
            currency,
            dataSource,
            name,
            symbol
          }) => {
            return {
              assetClass,
              assetSubClass,
              dataSource,
              name,
              symbol,
              currency: this.convertCurrency(currency),
              dataProviderInfo: this.getDataProviderInfo()
            };
          }
        )
    };
  }

  private convertCurrency(aCurrency: string) {
    let currency = aCurrency;

    if (currency === 'GBX') {
      currency = 'GBp';
    }

    return currency;
  }

  private convertFromEodSymbol(aEodSymbol: string) {
    let symbol = aEodSymbol;

    if (this.isForex(symbol)) {
      symbol = symbol.replace('GBX', 'GBp');
      symbol = symbol.replace('.FOREX', '');
    }

    return symbol;
  }

  /**
   * Converts a symbol to a EOD symbol
   *
   * Currency:  USDCHF  -> USDCHF.FOREX
   */
  private convertToEodSymbol(aSymbol: string) {
    if (
      aSymbol.startsWith(DEFAULT_CURRENCY) &&
      aSymbol.length > DEFAULT_CURRENCY.length
    ) {
      if (
        isCurrency(
          aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length)
        )
      ) {
        let symbol = aSymbol;
        symbol = symbol.replace('GBp', 'GBX');

        return `${symbol}.FOREX`;
      }
    }

    return aSymbol;
  }

  private formatName({ name }: { name: string }) {
    if (name) {
      for (const part of REPLACE_NAME_PARTS) {
        name = name.replace(part, '');
      }

      name = name.trim();
    }

    return name;
  }

  private async getSearchResult({
    query,
    requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
  }: {
    query: string;
    requestTimeout?: number;
  }) {
    let searchResult: (LookupItem & {
      assetClass: AssetClass;
      assetSubClass: AssetSubClass;
      isin: string;
    })[] = [];

    try {
      const queryParams = new URLSearchParams({
        api_token: this.apiKey
      });

      const response = await fetch(
        `${this.URL}/search/${query}?${queryParams.toString()}`,
        {
          signal: AbortSignal.timeout(requestTimeout)
        }
      ).then((res) => res.json());

      searchResult = response.map(
        ({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => {
          const { assetClass, assetSubClass } = this.parseAssetClass({
            Exchange,
            Type
          });

          return {
            assetClass,
            assetSubClass,
            isin,
            currency: this.convertCurrency(Currency),
            dataSource: this.getName(),
            name: this.formatName({ name }),
            symbol: `${Code}.${Exchange}`
          };
        }
      );
    } catch (error) {
      let message = error;

      if (['AbortError', 'TimeoutError'].includes(error?.name)) {
        message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${(
          this.configurationService.get('REQUEST_TIMEOUT') / 1000
        ).toFixed(3)} seconds`;
      }

      Logger.error(message, 'EodHistoricalDataService');
    }

    return searchResult;
  }

  private isForex(aCode: string) {
    return aCode?.endsWith('.FOREX') || false;
  }

  private parseAssetClass({
    Exchange,
    Type
  }: {
    Exchange: string;
    Type: string;
  }): {
    assetClass: AssetClass;
    assetSubClass: AssetSubClass;
  } {
    let assetClass: AssetClass;
    let assetSubClass: AssetSubClass;

    switch (Type?.toLowerCase()) {
      case 'common stock':
        assetClass = AssetClass.EQUITY;
        assetSubClass = AssetSubClass.STOCK;
        break;
      case 'currency':
        assetClass = AssetClass.LIQUIDITY;

        if (Exchange?.toLowerCase() === 'cc') {
          assetSubClass = AssetSubClass.CRYPTOCURRENCY;
        }

        break;
      case 'etf':
        assetClass = AssetClass.EQUITY;
        assetSubClass = AssetSubClass.ETF;
        break;
      case 'fund':
        assetClass = AssetClass.EQUITY;
        assetSubClass = AssetSubClass.MUTUALFUND;
        break;
    }

    return { assetClass, assetSubClass };
  }
}
